﻿using System;
using System.Collections.Generic;
using System.Net.Mail;
using System.Text;
using Foothill.Common;
using Foothill.Common.Logging;
using Foothill.DataAccess.Interface;
using Foothill.Model;
using Foothill.Service.Configuration;
using Foothill.Service.Email;
using Foothill.Service.SystemMessage;
using Foothill.Service.Validation;

namespace Foothill.Service.UserAccount
{
    public class AccountRegistrationManager : IAccountRegistrationManager
    {
        private readonly IValidationService _validationService;
        private readonly IUserAccountRepo _userAccountRepo;
        private readonly IStringGenerator _stringGenerator;
        private readonly IEmailSender _emailSender;
        private readonly IHashUtility _hashUtility;
        private readonly ISystemMessageService _systemMessageService;
        private readonly IHostSettingManager _hostSettingManager;

        private static IDictionary<NewAccountInfoValidationResult, string> _errorEnumToMessageCodeMap;

        public AccountRegistrationManager(IValidationService validationService, IUserAccountRepo userAccountRepo,
            IStringGenerator stringGenerator, IEmailSender emailSender, IHashUtility hashUtility,
            ISystemMessageService systemMessageService, IHostSettingManager hostSettingManager)
        {
            _validationService = validationService;
            _userAccountRepo = userAccountRepo;
            _stringGenerator = stringGenerator;
            _emailSender = emailSender;
            _hashUtility = hashUtility;
            _systemMessageService = systemMessageService;
            _hostSettingManager = hostSettingManager;

            InitializeErrorEnumToMessageCodeMapping();
        }

        private void InitializeErrorEnumToMessageCodeMapping()
        {
            _errorEnumToMessageCodeMap = new Dictionary<NewAccountInfoValidationResult, string>();
            _errorEnumToMessageCodeMap.Add(NewAccountInfoValidationResult.EmailInUse, SecurityMessageCode.EmailIsInUse);
            _errorEnumToMessageCodeMap.Add(NewAccountInfoValidationResult.InvalidEmail, SecurityMessageCode.InvalidEmail);
            _errorEnumToMessageCodeMap.Add(NewAccountInfoValidationResult.EmailActivationPending, SecurityMessageCode.EmailActivationPending);
            _errorEnumToMessageCodeMap.Add(NewAccountInfoValidationResult.InvalidPassword, SecurityMessageCode.InvalidPassword);
            _errorEnumToMessageCodeMap.Add(NewAccountInfoValidationResult.InvalidUsername, SecurityMessageCode.InvalidUsername);
            _errorEnumToMessageCodeMap.Add(NewAccountInfoValidationResult.PasswordsNotMatch, SecurityMessageCode.PasswordsNotMatch);
            _errorEnumToMessageCodeMap.Add(NewAccountInfoValidationResult.PasswordTooLong, SecurityMessageCode.PasswordTooLong);
            _errorEnumToMessageCodeMap.Add(NewAccountInfoValidationResult.PasswordTooShort, SecurityMessageCode.PasswordTooShort);
            _errorEnumToMessageCodeMap.Add(NewAccountInfoValidationResult.UsernameInUse, SecurityMessageCode.UsernameInUse);
        }

        public ValidateAccountRegistrationResult ValidateAccountRegistrationInfo(AccountRegistrationInfo acctRegInfo, ICollection<Tuple<UserAccountField, string>> invalidFieldsAndError)
        {
            ValidateAccountRegistrationResult validateResult = ValidateAccountRegistrationResult.OkToCreateNewAccount;
            string errorMsg = string.Empty;

            // trim all the fields before validating
            TrimAllFields(acctRegInfo);

            // email must be valid and not in use
            NewAccountInfoValidationResult acctRegInfoValidationResult = ValidateEmail(acctRegInfo.Email);
            if (acctRegInfoValidationResult != NewAccountInfoValidationResult.EmailOK)
            {
                if (acctRegInfoValidationResult == NewAccountInfoValidationResult.EmailActivationExpired)
                {
                    validateResult = ValidateAccountRegistrationResult.SendAnotherActivation;
                }
                else if (acctRegInfoValidationResult == NewAccountInfoValidationResult.EmailActivationPending)
                {
                    // pending activation, just return display error
                    errorMsg = GetAccountRegistrationInfoValidationErrorMessage(acctRegInfoValidationResult);
                    invalidFieldsAndError.Add(new Tuple<UserAccountField, string>(UserAccountField.Email, errorMsg));
                    return ValidateAccountRegistrationResult.DisplayError;
                }
                else
                {
                    // invalid email format or it is in use
                    errorMsg = GetAccountRegistrationInfoValidationErrorMessage(acctRegInfoValidationResult);
                    invalidFieldsAndError.Add(new Tuple<UserAccountField, string>(UserAccountField.Email, errorMsg));
                    validateResult = ValidateAccountRegistrationResult.DisplayError;
                }
            }

            // username must be valid and not in use
            acctRegInfoValidationResult = ValidateUsername(acctRegInfo.Username);
            if (acctRegInfoValidationResult != NewAccountInfoValidationResult.UsernameOK)
            {
                errorMsg = GetAccountRegistrationInfoValidationErrorMessage(acctRegInfoValidationResult);
                invalidFieldsAndError.Add(new Tuple<UserAccountField, string>(UserAccountField.Username, errorMsg));
                validateResult = ValidateAccountRegistrationResult.DisplayError;
            }

            acctRegInfoValidationResult = ValidatePasswords(acctRegInfo.Password, acctRegInfo.ConfirmPassword, invalidFieldsAndError);
            if (acctRegInfoValidationResult != NewAccountInfoValidationResult.PasswordOk)
            {
                validateResult = ValidateAccountRegistrationResult.DisplayError;
            }

            return validateResult;
        }

        public bool CreateNewAccount(AccountRegistrationInfo newAcctInfo)
        {
            UserAccountDto newAccount = CreateUserAccountDto(newAcctInfo);
            Guid activationCode = Guid.NewGuid();

            try
            {
                // send confirmation email first, only insert into database when Email is successfully sent 
                SendAccountActivationEmail(newAccount.Email, activationCode.ToString());
            }
            catch (Exception e)
            {
                Log.Error(FoothillModule.Service, "Error in creating new account, fail to send Confirmation Email.", e);
                throw new FailToCreateAccountException(_systemMessageService.GetSystemMessage(SystemMessageCategory.Security, SecurityMessageCode.ConfirmationEmailCannotBeSent), e);
            }

            try
            {
                newAccount.AccountStateId = (int)UserAccountState.ConfirmationEmailSent;
                newAccount.ActivationCode = activationCode;
                newAccount.ActivationEmailSentDateTime = DateTime.Now;

                // insert into database
                _userAccountRepo.CreateNewUserAccount(newAccount);
            }
            catch (Exception e)
            {
                Log.Error(FoothillModule.Service, "Error in creating new account, fail to insert into database.");
                throw new FailToCreateAccountException(_systemMessageService.GetSystemMessage(SystemMessageCategory.Security, SecurityMessageCode.FailToCreateNewAccount), e);
            }

            return true;
        }

        public bool SendAnotherActivation(AccountRegistrationInfo info)
        {
            UserAccountDto existingPendingAcct = _userAccountRepo.GetUserAccountByEmail(info.Email);
            info.Password = _hashUtility.HashText(info.Password);
            if (existingPendingAcct == null)
            {
                Log.Write(FoothillModule.Service, LogLevel.Info, "Cannot find pending user account by email: " + info.Email);
                return false;
            }

            Guid activationCode = Guid.NewGuid();
            try
            {
                SendAccountActivationEmail(info.Email, activationCode.ToString());
            }
            catch (Exception e)
            {
                Log.Error(FoothillModule.Service, "Error in sending another activation, fail to send Confirmation Email.", e);
                throw new FailToCreateAccountException(_systemMessageService.GetSystemMessage(SystemMessageCategory.Security, SecurityMessageCode.ConfirmationEmailCannotBeSent), e);
            }

            try
            {
                existingPendingAcct.AccountStateId = (int)UserAccountState.ConfirmationEmailSent;
                existingPendingAcct.ActivationCode = activationCode;
                existingPendingAcct.ActivationEmailSentDateTime = DateTime.Now;
                existingPendingAcct.Username = info.Username;
                existingPendingAcct.Password = info.Password;

                // update in the database
                _userAccountRepo.UpdatePendingAccountAfterSendingActivation(existingPendingAcct);
            }
            catch (Exception e)
            {
                Log.Error(FoothillModule.Service, "Error in creating new account, fail to insert into database.");
                throw new FailToCreateAccountException(_systemMessageService.GetSystemMessage(SystemMessageCategory.Security, SecurityMessageCode.FailToCreateNewAccount), e);
            }

            return true;
        }

        public AccountActivationResult VerifyActivationCode(object authCode, out int accountId, out string message)
        {
            AccountActivationResult activationResult = AccountActivationResult.InvalidToken;
            
            accountId = -1;
            if (authCode == null)
            {
                activationResult = AccountActivationResult.InvalidToken;
                message = _systemMessageService.GetSystemMessage(SystemMessageCategory.Security, GetMessageCodeFromActivationResult(activationResult));
                return activationResult;
            }

            Guid authGuid;
            if (!Guid.TryParse(authCode.ToString(), out authGuid))
            {
                activationResult = AccountActivationResult.InvalidToken;
                message = _systemMessageService.GetSystemMessage(SystemMessageCategory.Security, GetMessageCodeFromActivationResult(activationResult));
                return activationResult;
            }

            UserAccountDto userAccount = _userAccountRepo.GetUserAccountByActivationCode(authGuid);
            if (userAccount == null || userAccount.UserAccountId < 0 )
            {
                activationResult = AccountActivationResult.InvalidToken;
                message = _systemMessageService.GetSystemMessage(SystemMessageCategory.Security, GetMessageCodeFromActivationResult(activationResult));
                return activationResult;
            }

            if (userAccount.AccountStateId != (int)UserAccountState.ConfirmationEmailSent)
            {
                activationResult = AccountActivationResult.AlreadyActivated;
                message = _systemMessageService.GetSystemMessage(SystemMessageCategory.Security, GetMessageCodeFromActivationResult(activationResult));
                return activationResult;
            }

            if (IsActivationExpired(userAccount.ActivationEmailSentDateTime))
            {
                activationResult = AccountActivationResult.Expired;
            }
            else
            {
                activationResult = AccountActivationResult.Succeed;
            }

            accountId = userAccount.UserAccountId;
            message = _systemMessageService.GetSystemMessage(SystemMessageCategory.Security, GetMessageCodeFromActivationResult(activationResult));
            return activationResult;
        }

        public bool ActivateNewAccount(int accountId)
        {
            UserAccountDto userAccount = new UserAccountDto
            {
                UserAccountId = accountId,
                AccountStateId = (int)UserAccountState.Active,
                DateActivated = DateTime.Now
            };

            _userAccountRepo.ActivateUserAccount(userAccount);
            return true;
        }

        internal string GetMessageCodeFromActivationResult(AccountActivationResult activationResult)
        {
            string result = SecurityMessageCode.AcctActivation_InvalidToken;

            switch (activationResult)
            {
                case AccountActivationResult.AlreadyActivated:
                    result = SecurityMessageCode.AcctActivation_AlreadyActivated;
                    break;
                case AccountActivationResult.Expired:
                    result = SecurityMessageCode.AcctActivation_Expired;
                    break;
                case AccountActivationResult.InvalidToken:
                    result = SecurityMessageCode.AcctActivation_InvalidToken;
                    break;
                case AccountActivationResult.Succeed:
                    result = SecurityMessageCode.AcctActivation_Succeeded;
                    break;
            }

            return result;
        }

        internal void SendAccountActivationEmail(string toEmailAddress, string activationCode)
        {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.AppendLine("您好");
            stringBuilder.AppendLine("欢迎注册，请点击");
            stringBuilder.AppendLine(BuildActivationLink(activationCode));

            using (MailMessage mailMessage = new MailMessage())
            {
                mailMessage.Subject = "欢迎注册";
                mailMessage.Body = stringBuilder.ToString();
                mailMessage.IsBodyHtml = true;

                mailMessage.To.Add(new MailAddress(toEmailAddress));

                _emailSender.SendEmail(mailMessage);
            }
        }

        internal string BuildActivationLink(string activationCode)
        {
            Uri uri = new Uri(_hostSettingManager.GetSiteBaseUri(), "AccountActivation/AccountActivation.aspx?" + GlobalQueryString.AuthCodeQsName+ "=" + activationCode);
            return uri.ToString();  
        }

        // check the format and if it can be found in database
        internal NewAccountInfoValidationResult ValidateEmail(string email)
        {
            NewAccountInfoValidationResult validationResult = NewAccountInfoValidationResult.EmailOK;
            
            if (!_validationService.ValidateEmailAddress(email))
            {
                // invalid email format
                validationResult = NewAccountInfoValidationResult.InvalidEmail;
            }
            else
            {
                // email format is valid, check to see if it is already in use
                UserAccountDto userAcct =_userAccountRepo.GetUserAccountByEmail(email);

                if (userAcct != null)
                {
                    switch ((UserAccountState)userAcct.AccountStateId)
                    {
                        case UserAccountState.Active:
                        case UserAccountState.UsingTempPassword:
                            validationResult = NewAccountInfoValidationResult.EmailInUse;
                            break;
                        case UserAccountState.ConfirmationEmailSent:
                        case UserAccountState.ConfirmationEmailExpire:
                            if (IsActivationExpired(userAcct.ActivationEmailSentDateTime))
                            {
                                // there is a pending account whose activation email is expired
                                validationResult = NewAccountInfoValidationResult.EmailActivationExpired;
                            }
                            else
                            {
                                // pending activation is still valid
                                validationResult = NewAccountInfoValidationResult.EmailActivationPending;
                            }
                            break;
                    }
                }
            }

            return validationResult;
        }

        internal NewAccountInfoValidationResult ValidateUsername(string username)
        {
            NewAccountInfoValidationResult validationResult = NewAccountInfoValidationResult.UsernameOK;
            if (string.IsNullOrWhiteSpace(username))
            {
                validationResult = NewAccountInfoValidationResult.InvalidUsername;
            }
            else
            {
                UserAccountDto userAcctDetail = _userAccountRepo.GetUserAccountDetailsByUserName(username);

                if (userAcctDetail != null)
                {
                    switch ((UserAccountState)userAcctDetail.AccountStateId)
                    {
                        case UserAccountState.Active:
                        case UserAccountState.UsingTempPassword:
                            validationResult = NewAccountInfoValidationResult.UsernameInUse;
                            break;
                        case UserAccountState.ConfirmationEmailSent:

                            if (IsActivationExpired(userAcctDetail.ActivationEmailSentDateTime))
                            {
                                // there is a pending account whose activation email is expired, delete it
                                _userAccountRepo.DeleteUserAccount(userAcctDetail.UserAccountId);
                            }
                            else
                            {
                                // there is a pending account whose activation email is still valid
                                validationResult = NewAccountInfoValidationResult.UsernameInUse;
                            }

                            break;
                    }
                }
            }

            return validationResult;
        }

        internal NewAccountInfoValidationResult ValidatePasswords(string pwd1, string pwd2, ICollection<Tuple<UserAccountField, string>> invalidFieldsAndError)
        {
            string errorMsg;
            NewAccountInfoValidationResult validateResult = ValidatePassword(pwd1);
            if (validateResult != NewAccountInfoValidationResult.PasswordOk)
            {
                errorMsg = GetAccountRegistrationInfoValidationErrorMessage(validateResult);
                invalidFieldsAndError.Add(new Tuple<UserAccountField, string>(UserAccountField.Password, errorMsg));
            }

            // two passwords should match
            if (string.Compare(pwd1, pwd2, false) != 0)
            {
                errorMsg = GetAccountRegistrationInfoValidationErrorMessage(NewAccountInfoValidationResult.PasswordsNotMatch);
                invalidFieldsAndError.Add(new Tuple<UserAccountField, string>(UserAccountField.ConfirmPassword, errorMsg));
                validateResult = NewAccountInfoValidationResult.PasswordsNotMatch;
            }

            return validateResult;
        }

        // password cannot be empty and must be within length range
        internal NewAccountInfoValidationResult ValidatePassword(string pwd)
        {
            NewAccountInfoValidationResult validateResult = NewAccountInfoValidationResult.PasswordOk;
            
            PasswordFormatError passwordFormatError;
            if (_validationService.ValidatePassword(pwd, out passwordFormatError))
            {
                return NewAccountInfoValidationResult.PasswordOk;   
            }
            else
            {
                switch (passwordFormatError)
                {
                    case PasswordFormatError.IsEmpty:
                        validateResult = NewAccountInfoValidationResult.InvalidPassword;
                        break;
                    case PasswordFormatError.TooLong:
                        validateResult = NewAccountInfoValidationResult.PasswordTooLong;
                        break;
                    case PasswordFormatError.TooShort:
                        validateResult = NewAccountInfoValidationResult.PasswordTooShort;
                        break;
                }

                return validateResult;
            }
        }

        // get the error string based on the error code
        internal string GetAccountRegistrationInfoValidationErrorMessage(NewAccountInfoValidationResult errorEnum)
        {
            string messageCode = null;

            if (!_errorEnumToMessageCodeMap.TryGetValue(errorEnum, out messageCode))
            {
                return string.Empty;
            }

            return _systemMessageService.GetSystemMessage(SystemMessageCategory.Security, messageCode);
        }

        internal bool IsActivationExpired(DateTime activationEmailSentDateTime)
        {
            return (DateTime.Now - activationEmailSentDateTime).Days > GlobalSettings.ActivateAccountTokenExpiryDay;
        }

        private UserAccountDto CreateUserAccountDto(AccountRegistrationInfo newAcctInfo)
        {
            UserAccountDto result = new Model.UserAccountDto();

            result.Email = newAcctInfo.Email;
            result.Username = newAcctInfo.Username;
            result.Password = _hashUtility.HashText(newAcctInfo.Password);

            return result;
        }

        private void TrimAllFields(AccountRegistrationInfo acctRegInfo)
        {
            acctRegInfo.Email = acctRegInfo.Email.Trim();
            acctRegInfo.Username = acctRegInfo.Username.Trim();
            acctRegInfo.Password = acctRegInfo.Password.Trim();
            acctRegInfo.ConfirmPassword = acctRegInfo.ConfirmPassword.Trim();
        }
    }
}
